package models

import (
	"fmt"
	"github.com/go-ini/ini"
	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/mysql"
	//_ "github.com/jinzhu/gorm/dialects/sqlite3"
	"log"
	"strings"
	"sync"
	"time"
)

var DB *gorm.DB
var INIT_CHANNEL chan bool
var init_status = false

type NullType byte

var DbList map[string]*gorm.DB

const (
	_ NullType = iota
	// IsNull the same as `is null`
	IsNull
	// IsNotNull the same as `is not null`
	IsNotNull
)

var mutex sync.Mutex

// Setup initializes the database instance
func Setup() {
	if init_status {
		return
	}

	go func() {
		var err error
		cfg, err := ini.Load("conf/mysql.ini")
		if err != nil {
			log.Fatalf("fail to parse 'conf/mysql.ini': %v", err)
		}
		sections := cfg.Sections()
		DbList = make(map[string]*gorm.DB, len(sections))
		db_channel := make(chan string, len(sections))
		for _, v := range sections[1:] {

			go func(v *ini.Section) {
				userNmae, err := v.GetKey("username")
				if err != nil {
					db_channel <- fmt.Sprintf("models.Setup err: %v", err)
					return
				}
				password, err := v.GetKey("password")
				if err != nil {
					db_channel <- fmt.Sprintf("models.Setup err: %v", err)
					return
				}
				host, err := v.GetKey("host")
				if err != nil {
					db_channel <- fmt.Sprintf("models.Setup err: %v", err)
					return
				}
				dbname, err := v.GetKey("dbname")
				if err != nil {
					db_channel <- fmt.Sprintf("models.Setup err: %v", err)
					return
				}
				go func(userNmae, password, host, dbname *ini.Key) {
					db, err := gorm.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=True&loc=Local",
						userNmae,
						password,
						host,
						dbname))

					if err != nil {
						db_channel <- fmt.Sprintf("models.Setup err: %v", err)
						return
					}
					db.LogMode(true)
					db.SingularTable(true)
					db.DB().SetMaxIdleConns(10)
					db.DB().SetMaxOpenConns(100)
					register_db(v.Name(), db)
					db_channel <- fmt.Sprintf("%s : success", v.Name())
				}(userNmae, password, host, dbname)
			}(v)
		}
		i := 0
		for a := range db_channel {
			log.Println(a)
			i++
			if len(sections)-1 == i {
				close(db_channel)
			}
		}

		INIT_CHANNEL <- true
		gorm.DefaultTableNameHandler = func(db *gorm.DB, defaultTableName string) string {
			return "" + defaultTableName
		}

	}()
}

func register_db(name string, db *gorm.DB) {
	mutex.Lock()
	defer mutex.Unlock()
	DbList[name] = db
}

func GetDB(dbName string) *gorm.DB {

	if db, ok := DbList[dbName]; ok {
		return db
	}

	if !init_status {
		INIT_CHANNEL = make(chan bool)
		go Setup()
		init_status = <-INIT_CHANNEL
		close(INIT_CHANNEL)
	}

	if db, ok := DbList[dbName]; ok {
		return db
	}

	panic(fmt.Sprintf("%s:error ", dbName))

	return nil
}

// CloseDB closes database connection (unnecessary)
func CloseDB() {
	if !init_status {
		return
	}
	defer func() {
		for _, v := range DbList {
			v.Close()
		}
	}()
}

// updateTimeStampForCreateCallback will set `CreatedOn`, `ModifiedOn` when creating
func updateTimeStampForCreateCallback(scope *gorm.Scope) {
	if !scope.HasError() {
		nowTime := time.Now().Unix()
		if createTimeField, ok := scope.FieldByName("CreatedOn"); ok {
			if createTimeField.IsBlank {
				createTimeField.Set(nowTime)
			}
		}

		if modifyTimeField, ok := scope.FieldByName("ModifiedOn"); ok {
			if modifyTimeField.IsBlank {
				modifyTimeField.Set(nowTime)
			}
		}
	}
}

// updateTimeStampForUpdateCallback will set `ModifiedOn` when updating
func updateTimeStampForUpdateCallback(scope *gorm.Scope) {
	if _, ok := scope.Get("gorm:update_column"); !ok {
		scope.SetColumn("ModifiedOn", time.Now().Unix())
	}
}

// deleteCallback will set `DeletedOn` where deleting
func deleteCallback(scope *gorm.Scope) {
	if !scope.HasError() {
		var extraOption string
		if str, ok := scope.Get("gorm:delete_option"); ok {
			extraOption = fmt.Sprint(str)
		}

		deletedOnField, hasDeletedOnField := scope.FieldByName("DeletedOn")

		if !scope.Search.Unscoped && hasDeletedOnField {
			scope.Raw(fmt.Sprintf(
				"UPDATE %v SET %v=%v%v%v",
				scope.QuotedTableName(),
				scope.Quote(deletedOnField.DBName),
				scope.AddToVars(time.Now().Unix()),
				addExtraSpaceIfExist(scope.CombinedConditionSql()),
				addExtraSpaceIfExist(extraOption),
			)).Exec()
		} else {
			scope.Raw(fmt.Sprintf(
				"DELETE FROM %v%v%v",
				scope.QuotedTableName(),
				addExtraSpaceIfExist(scope.CombinedConditionSql()),
				addExtraSpaceIfExist(extraOption),
			)).Exec()
		}
	}
}

// addExtraSpaceIfExist adds a separator
func addExtraSpaceIfExist(str string) string {
	if str != "" {
		return " " + str
	}
	return ""
}

func WhereBuild(where map[string]interface{}) (whereSQL string, vals []interface{}, err error) {
	for k, v := range where {
		ks := strings.Split(k, " ")
		if len(ks) > 2 {
			return "", nil, fmt.Errorf("Error in query condition: %s. ", k)
		}

		if whereSQL != "" {
			whereSQL += " AND "
		}
		strings.Join(ks, ",")
		switch len(ks) {
		case 1:
			//fmt.Println(reflect.TypeOf(v))
			switch v := v.(type) {
			case NullType:
				if v == IsNotNull {
					whereSQL += fmt.Sprint(k, " IS NOT NULL")
				} else {
					whereSQL += fmt.Sprint(k, " IS NULL")
				}
			default:
				whereSQL += fmt.Sprint(k, "=?")
				vals = append(vals, v)
			}
			break
		case 2:
			k = ks[0]
			switch ks[1] {
			case "=":
				whereSQL += fmt.Sprint(k, "=?")
				vals = append(vals, v)
				break
			case ">":
				whereSQL += fmt.Sprint(k, ">?")
				vals = append(vals, v)
				break
			case ">=":
				whereSQL += fmt.Sprint(k, ">=?")
				vals = append(vals, v)
				break
			case "<":
				whereSQL += fmt.Sprint(k, "<?")
				vals = append(vals, v)
				break
			case "<=":
				whereSQL += fmt.Sprint(k, "<=?")
				vals = append(vals, v)
				break
			case "!=":
				whereSQL += fmt.Sprint(k, "!=?")
				vals = append(vals, v)
				break
			case "<>":
				whereSQL += fmt.Sprint(k, "!=?")
				vals = append(vals, v)
				break
			case "in":
				whereSQL += fmt.Sprint(k, " in (?)")
				vals = append(vals, v)
				break
			case "notin":
				whereSQL += fmt.Sprint(k, " not in (?)")
				vals = append(vals, v)
				break
			case "like":
				whereSQL += fmt.Sprint(k, " like ?")
				vals = append(vals, v)
			}
			break
		}
	}
	return
}
